package com.mebs.edi.parser.dom;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.log4j.Logger;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.mebs.edi.model.dom.DomAttribute;
import com.mebs.edi.model.dom.DomChild;
import com.mebs.edi.model.dom.DomChildren;
import com.mebs.edi.model.dom.DomEntity;
import com.mebs.edi.parser.utils.ClassUtils;

/**
 * 
 * This class converts DOM objects to their corresponding classes 
 * by using DOM mapping annotations 
 * (see 'model' module for annotation definitions and model classes mapping)
 * 
 * @author nit
 * @version 1.0
 * 
 */
public class DomToModelConverter extends DomConverter {
	
	private static final Logger LOG = Logger.getLogger(DomToModelConverter.class);
	
	public DomToModelConverter() {
		LOG.debug("Instantiating a DOM to Model converter");
	}
	
	/**
	 * 
	 * Converts a DOM Node object into an annotated class instance
	 * 
	 * @param domNode
	 * @return annotated model object
	 * @throws ConvertionException
	 * 
	 */
	public Object convert(Element domNode) throws ConvertionException {
		LOG.info("Converting " + domNode);
		
		Class model = getCorrespondingClass(domNode);
		
		if (model == null) {
			throw new IllegalArgumentException("Element " + domNode + " does not have a corresponding model class; " +
					"use DomConverter.addAnnotatedClass to register appropriate model");
		}
		
		try {
			Object modelObject = model.newInstance();
			
			List<Field> fields = getFields(model);
			
			for (Field field : fields) {
				
				// mapping DOM attribute to instance field
				DomAttribute attributeAnnotation = field.getAnnotation(DomAttribute.class);
				if (attributeAnnotation != null) {
					processFieldFromAttribute(field, domNode, modelObject);
					continue;
				}
				
				// mapping child node to instance field
				DomChild childAnnotation = field.getAnnotation(DomChild.class);
				if (childAnnotation != null) {
					processFieldFromNode(field, domNode, modelObject);
					continue;
				}
				
				// mapping multiple children nodes to instance field
				DomChildren childrenAnnotation = field.getAnnotation(DomChildren.class);
				if (childrenAnnotation != null) {
					processCollectionFieldFromNode(field, domNode, modelObject);
					continue;
				}
			}
		
			return modelObject;
			
		} catch (Exception e) { 
			throw new ConvertionException("Convertion failed ", e);
		}
	}


	/**
	 * 
	 * Extracts node attribute value and assigns it to corresponding field of 'model' object
	 * 
	 * @param field - field of the model object
	 * @param node - DOM node, holding the attribute
	 * @param model - object, holding the field
	 * @throws Exception
	 */
	private void processFieldFromAttribute(Field field, Element node, Object model) throws Exception {
		DomAttribute attribute = field.getAnnotation(DomAttribute.class);
		
		String attributeName = getAttributeName(field);

		String attributeValue;
		attributeValue = node.getAttribute(attributeName);
		
		if (!node.hasAttribute(attributeName)) {
			// attribute not specified
			return;
		}

		Class setterParameterClass = field.getType();
		Object setterParameter = ClassUtils.convertStringToClass(attributeValue, setterParameterClass);
		
		Method setterMethod = ClassUtils.getSetterMethod(field, model.getClass());
		setterMethod.invoke(model, setterParameter);
	}
	
	/**
	 * 
	 * Converts element's child node and assigns the result of conversion
	 * 
	 * @param field
	 * @param parentNode
	 * @param model
	 * @throws Exception
	 */
	private void processFieldFromNode(Field field, Element parentNode, Object model) throws Exception {
		Class childClass = field.getType();
		DomEntity entityAnnotation = (DomEntity)childClass.getAnnotation(DomEntity.class);
		
		if (entityAnnotation == null) {
			throw new ConvertionException("Child entity has no mapping");
		}
		
		DomChild childAnnotation = field.getAnnotation(DomChild.class);
		Integer offset = childAnnotation.offset();
		
		String childName = entityAnnotation.name();
		Element childElement = getNestedChild(childName, parentNode, offset);
		Object childObject = convert(childElement);	
		
		Method setterMethod = ClassUtils.getSetterMethod(field, model.getClass());
		setterMethod.invoke(model, childObject);
	}
	
	/**
	 * Converts several children nodes, adds them to a collection, and assigns this collection to parent element
	 * 
	 * @param field
	 * @param parentNode
	 * @param model
	 * @throws Exception
	 */
	private void processCollectionFieldFromNode(Field field, Element parentNode, Object model) throws Exception {
		ParameterizedType pType = (ParameterizedType)field.getGenericType();
		
		Class childClass = (Class)pType.getActualTypeArguments()[0];
		DomEntity entityAnnotation = (DomEntity)childClass.getAnnotation(DomEntity.class);
		
		if (entityAnnotation == null) {
			throw new ConvertionException("Child entity has no mapping");
		}
		
		String childName = entityAnnotation.name();
		List<Object> childrenObject = new ArrayList<Object>();
		
		NodeList children = parentNode.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if (childName.equalsIgnoreCase(child.getLocalName())) {
				Object childObject = convert((Element)child);
				childrenObject.add(childObject);
			}
		}
		
		Method setterMethod = ClassUtils.getSetterMethod(field, model.getClass());
		setterMethod.invoke(model, childrenObject);
	}
	
	/**
	 * Gets nested child of a DOM Node
	 * 
	 * @param name
	 * @param parentNode
	 * @return
	 */
	private Element getNestedChild(String name, Element parentNode) {
		return getNestedChild(name, parentNode, 0);
	}
	
	/**
	 * Gets nested child of a DOM Node
	 * 
	 * @param name
	 * @param parentNode
	 * @param offset - number of the child
	 * @return
	 */
	private Element getNestedChild(String name, Element parentNode, int offset) {
		int offsetCounter = 0;
		NodeList children = parentNode.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if (name.equalsIgnoreCase(child.getLocalName())) {
				if (offsetCounter == offset) {
					return (Element)child;
				} else {
					offsetCounter++;
				}
			}
		}
		return null;
	}
	
	/**
	 * Check annotated classes and return the class, that matches provided XML node name.
	 * In case if more than one class match the same name, discriminator attribute is used 
	 * (see DomEntity annotation)
	 * 
	 * @param name
	 * @param domNode
	 * @return
	 */
	private Class getCorrespondingClass(Element domNode) {
		String name = domNode.getLocalName();
		Class model = null;
		
		for (DomEntity entityAnnotation : annotatedClasses.keySet()) {
			if (name.equalsIgnoreCase(entityAnnotation.name())) {
				
				// remembering this class to return if no better match is found
				model = annotatedClasses.get(entityAnnotation);
				
				// check discriminator attribute
				String discriminatorAttibute = entityAnnotation.discriminatorAttribute();
				if (!"".equalsIgnoreCase(discriminatorAttibute)) {
					// discriminator is present
					
					String[] discriminatorValues = entityAnnotation.discriminatorValues();
					if (discriminatorValues.length == 0 && domNode.hasAttribute(discriminatorAttibute)) {
						// do not check the value, attribute presence is enough
						return annotatedClasses.get(entityAnnotation);
						
					} else {
						String attribute = domNode.getAttribute(discriminatorAttibute);
						// loop through all possible discriminators to see if a match can be found
						for (String discriminatorValue : discriminatorValues) {
							if (discriminatorValue.equalsIgnoreCase(attribute)) {
								// exact match is found, returning
								return annotatedClasses.get(entityAnnotation);
							}
						}
					}
				}
			}
		}
		
		return model;
	}
}
